fix: delay anchor line scrolling until layout settles#7544
fix: delay anchor line scrolling until layout settles#7544JohnMcLear merged 4 commits intoether:developfrom
Conversation
Review Summary by QodoDelay anchor line scrolling until layout settles
WalkthroughsDescription• Refactor anchor line scrolling to reapply focus until layout settles • Extract line number parsing and focus logic into separate functions • Add 10-second settle window with 250ms polling intervals • Replace animation with direct scrollTop for better reliability • Add regression tests for delayed content expansion scenarios Diagramflowchart LR
A["Hash URL with #L anchor"] -->|getHashedLineNumber| B["Extract line number"]
B -->|focusOnLine| C["Start settle loop"]
C -->|250ms interval| D["getCurrentTargetOffset"]
D -->|focusOnHashedLine| E["Scroll and set selection"]
E -->|until 10s timeout| F["Layout stabilized"]
File Changes1. src/static/js/pad_editor.ts
|
Code Review by Qodo
1. Wrong line DOM selector
|
| const focusUntilStable = () => { | ||
| if (Date.now() - startTime >= maxSettleDuration) { | ||
| window.clearInterval(intervalId); | ||
| return; | ||
| } | ||
| } | ||
| const currentOffsetTop = getCurrentTargetOffset(); | ||
| if (currentOffsetTop == null) return; | ||
|
|
||
| focusOnHashedLine(ace, lineNumberInt); | ||
| }; | ||
|
|
||
| focusUntilStable(); | ||
| intervalId = window.setInterval(focusUntilStable, settleInterval); |
There was a problem hiding this comment.
1. Immediate focusuntilstable() scroll 📎 Requirement gap ≡ Correctness
The new #L... behavior scrolls immediately on load via focusUntilStable() instead of deferring until the pad layout has fully settled. This can still cause an initial incorrect viewport position before late-loading content (for example images) finishes affecting layout.
Agent Prompt
## Issue description
The implementation scrolls immediately and then re-applies scrolling for a fixed settle window, but it does not actually *defer* scrolling until the layout has stabilized.
## Issue Context
Compliance requires that `#L...` navigation scroll occurs only after async content (e.g., images/plugins) has finished loading and the resulting layout is stable, to prevent landing on the wrong line/region.
## Fix Focus Areas
- src/static/js/pad_editor.ts[271-283]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (lineNumber) { | ||
| if (lineNumber[0] === 'L') { | ||
| const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody'); | ||
| const lineNumberInt = parseInt(lineNumber.substr(1)); |
There was a problem hiding this comment.
I must say this code is quite a mess. I like the new style with better function naming 😊.
| } | ||
| } | ||
| if (!lineNumber || lineNumber[0] !== 'L') return null; | ||
| const lineNumberInt = parseInt(lineNumber.substr(1)); |
There was a problem hiding this comment.
Just fyi. Substr should not be used as it is deprecated. You can use substring which should work in your case identically. You only need to pay attention because the second argument does not anymore mean length but end position. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
| const hasMobileLayout = $('body').hasClass('mobile-layout'); | ||
| if (!hasMobileLayout) offsetTop += parseInt($inner.css('padding-top').replace('px', '')); | ||
| const $outerdocHTML = $aceOuter.contents().find('#outerdocbody').parent(); | ||
| $outerdoc.css({top: `${offsetTop}px`}); // Chrome |
There was a problem hiding this comment.
Is this only for chrome? Is it still relevant?
Co-authored-by: Copilot <[email protected]>
Addresses Qodo review: the 10s reapply loop could fight the user when they tried to scroll or click away from the anchored line. Listen for wheel/touchmove/keydown/mousedown on both ace_outer and ace_inner documents in capture phase and tear down the interval on first signal. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
de449b7 to
157cc5f
Compare
Addresses Qodo review on ether#7544: 1. Requirement gap (#1): Add stability detection to focusOnLine()'s reapply loop. When the target line's offsetTop has not changed for 3 consecutive 250ms ticks (~750ms), stop() is called early instead of running the full 10s window. This means once late content is no longer shifting layout, the loop releases the user immediately rather than waiting out maxSettleDuration. 2. Maintainability (ether#4): Add a comment explaining why the previous $.animate({scrollTop}) "needed for FF" path was replaced with a direct .scrollTop() call — the settle interval now covers the late-layout case Firefox originally needed animation for. Also adds a test that the reapply loop exits early so a user-initiated scrollTop=0 after ~2s is not reverted by another reapply tick. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
/review |
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
|
Persistent review updated to latest commit 0060805 |
…-out Round 3 of Qodo review on ether#7544: ether#3 Early exit misses late shifts — image loads / plugin renders past my previous 750ms early-exit window were no longer corrected. Add a `minSettleDuration` of 2s before any early-exit can fire, and bump `stableTicksRequired` from 3 to 4. Hard ceiling stays 10s. ether#4 Offset equality prevents stability — strict === on `offset().top` never matched in the presence of sub-pixel rounding, so the loop ran the full 10s even on stable layouts. Switch to `Math.abs(...) < 1` tolerance. ether#7 Invalid anchors spin interval — when `getCurrentTargetOffset()` keeps returning null (the requested line never resolves), the loop used to run for the full 10s doing nothing. Track consecutive misses and `stop()` after `missingTicksRequired` (8 ticks ≈ 2s). Real "inner doc not yet rendered" cases get the first 2s window. Bump the early-exit test's wait from 2s → 3.5s to clear the new `minSettleDuration` + `stableTicksRequired` window before asserting. Pushing back on remaining Qodo items: #1 Defer scroll until layout settles — the design is "scroll once immediately so the user sees the line, then keep correcting". Deferring all scrolling until "stable" (which is unknowable up front) would visibly hang on `#L...` navigation for seconds while nothing happens. The reapply loop is the deferral. ether#6 FF rationale lost — already addressed in the previous commit (comment on the `scrollTop()` call explaining why the `$.animate({scrollTop})` "needed for FF" path was removed). Qodo's persistent review doesn't track resolution of items that aren't touched by the new commit. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
/review |
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
|
Persistent review updated to latest commit b1d2156 |
| const $inner = $aceOuter.contents().find('iframe').contents().find('#innerdocbody'); | ||
| const line = $inner.find(`div:nth-child(${lineNumberInt})`); | ||
| if (line.length === 0) return false; |
There was a problem hiding this comment.
1. Wrong line dom selector 🐞 Bug ≡ Correctness
focusOnHashedLine() and the settle loop use $inner.find('div:nth-child(N)'), which can match
nested <div> elements inside earlier lines and scroll/select the wrong element. This breaks #L…
navigation for pads/plugins that introduce nested divs in line content.
Agent Prompt
### Issue description
The anchor scroll logic locates the target line via `$inner.find(`div:nth-child(${lineNumberInt})`)`, which searches all descendant divs and can select nested divs inside earlier lines. This can cause scrolling and `ace_setSelection()` to target the wrong node.
### Issue Context
In the inner iframe, `#innerdocbody` is cleared and then populated with the editor line nodes as direct children. Plugins can mutate each line node’s HTML (`acePostWriteDomLineHTML`), including inserting nested `<div>` elements.
### Fix Focus Areas
- src/static/js/pad_editor.ts[220-257]
- src/static/js/pad_editor.ts[259-268]
### Implementation notes
- Replace the descendant selector with a direct-child selection, for example:
- `const $line = $inner.children('div').eq(lineNumberInt - 1);`
- or `const $line = $inner.find(`> div:nth-child(${lineNumberInt})`);`
- Apply the same change in both `focusOnHashedLine()` and `getCurrentTargetOffset()` so the settle loop and the actual focus use the same element.
- Keep the existing `length === 0` guard and continue using the resolved node for `ace_setSelection()`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
#L...line scrolling for a short settle window after pad loadCloses #5700